/* Copyright (c) 2008 Erik Beeson
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted
 * provided that the following conditions are met:
 *
 * Redistributions of source code must retain the
 *     above copyright notice, this list of conditions
 *     and the following disclaimer.
 * Redistributions in binary form must reproduce the
 *     above copyright notice, this list of conditions
 *     and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 * Neither the name of Erik Beeson nor YourSharade nor the
 *     names of its contributors may be used to endorse or
 *     promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.restly.servlet;

import net.sf.oval.ConstraintViolation;
import net.sf.oval.Validator;
import net.sf.oval.localization.message.ResourceBundleMessageResolver;
import org.restly.*;
import org.restly.core.FileLocator;
import org.restly.core.InterceptorChain;
import org.restly.core.ResourceInfo;
import org.restly.core.RestlyValidator;
import org.restly.representations.SeeOtherRepresentation;
import org.restly.representations.SerializedRepresentation;
import org.restly.representations.StringRepresentation;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * Oct 24, 2008, 2:14:10 AM
 *
 * @author <a href="http://restly.org/">Erik Beeson</a>
 */
public class RestlyFilter implements Filter {
	private static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";

	private RestlyContext context;

	private List<ResourceInfo> resources = new ArrayList<ResourceInfo>();

	public void init(FilterConfig config) throws ServletException {
		ResourceBundleMessageResolver resolver = (ResourceBundleMessageResolver) Validator.getMessageResolver();
		for(String properties : FileLocator.getFilenames("OValMessages.properties")) {
			resolver.addMessageBundle(ResourceBundle.getBundle(properties.substring(0, properties.length() - ".properties".length())));
		}

		for(Class clazz : org.restly.core.FileLocator.getClasses()) {
			if(clazz.isAnnotationPresent(Resource.class)) {
				resources.add(new ResourceInfo(clazz));
			}
		}

		context = new RestlyContext(config);
	}

	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;

		String uri = extractPathFromRequestURI(request);

		boolean handled = false;

		for(Iterator<org.restly.core.ResourceInfo> it = resources.iterator(); !handled && it.hasNext();) {
			org.restly.core.ResourceInfo resourceInfo = it.next();
			Map<String, String> pathAttributes = resourceInfo.matches(uri);
			if(pathAttributes != null) {
				invoke(resourceInfo.getClazz(), pathAttributes, request, response);
				handled = true;
			}
		}

		if(!handled) {
			chain.doFilter(servletRequest, servletResponse);
		}
	}

	public void destroy() {
	}

	@SuppressWarnings("unchecked")
	public void invoke(Class clazz, Map<String, String> pathAttributes, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
			throws ServletException {
		RestlyHttpServletRequest request = new RestlyHttpServletRequest(httpServletRequest, clazz, pathAttributes);
		RestlyHttpServletResponse response = new RestlyHttpServletResponse(httpServletResponse, request.isHead());

		try {
			Method method = null;

			Method[] methods = clazz.getMethods();
			for(int i = 0; i < methods.length && method == null; i++) {
				Method m = methods[i];
				if((request.isHead() && "head".equalsIgnoreCase(m.getName())) || m.getName().equalsIgnoreCase(request.getMethod())) {
					method = m;
				}
			}

			Representation representation = null;

			if(method != null) {
				if(request.getPreferredContentType() == null) {
					response.setStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
				} else {
					List<Interceptor> interceptors = new ArrayList<Interceptor>();

					if(clazz.isAnnotationPresent(InterceptedBy.class)) {
						for(Class<? extends Interceptor> interceptorClass : ((InterceptedBy) clazz.getAnnotation(InterceptedBy.class)).value()) {
							interceptors.add(interceptorClass.newInstance());
						}
					}

					if(method.isAnnotationPresent(InterceptedBy.class)) {
						for(Class<? extends Interceptor> interceptorClass : method.getAnnotation(InterceptedBy.class).value()) {
							interceptors.add(interceptorClass.newInstance());
						}
					}

					InterceptorChain chain = new InterceptorChain(interceptors);

					representation = chain.next(request, response);

					if(representation == null) {
						Object instance = clazz.newInstance();

						for(Field field : getFields(clazz)) {
							field.setAccessible(true);

							if(field.getType().isInstance(request)) {
								field.set(instance, request);
							} else if(field.getType().isInstance(response)) {
								field.set(instance, response);
							} else if(pathAttributes.containsKey(field.getName())) {
								field.set(instance, pathAttributes.get(field.getName()));
							} else if(request.getAttribute(field.getName()) != null) {
								field.set(instance, request.getAttribute(field.getName()));
							} else if(Modifier.isPublic(field.getModifiers()) &&
									request.getParameter(field.getName()) != null) {
								field.set(instance, convertValue(field.getType(), request.getParameter(field.getName())));
							}
						}

						List<String> errors = new ArrayList<String>();

						RestlyValidator validator = new RestlyValidator(request);

						for(ConstraintViolation constraintViolation : validator.validate(instance)) {
							errors.add(constraintViolation.getMessage().replaceAll(clazz.getName() + ".", ""));
						}

						if(errors.size() > 0) {
							response.setStatus(HttpStatus.BAD_REQUEST);
							representation = new SerializedRepresentation(new ErrorMessages(errors));
						} else {
							representation = buildRepresentation(method.invoke(instance), request, response);

							for(Interceptor interceptor : interceptors) {
								representation = interceptor.after(request, response, representation);
							}
						}
					}
				}
			} else {
				if(!request.isOptions()) {
					response.setStatus(HttpStatus.METHOD_NOT_ALLOWED);
				}
				response.setHeader("Accept", getAcceptString(clazz));
			}

			if(representation == null) {
				response.setStatus(HttpStatus.NO_CONTENT);
			} else {
				representation.represent(context, request, response);
			}
		} catch(Exception e) {
			throw new ServletException(e);
		}
	}

	protected static Representation buildRepresentation(Object o, RestlyHttpServletRequest request, RestlyHttpServletResponse response) {
		if(o instanceof Representation) {
			return (Representation) o;
		} else if(o instanceof HttpStatus) {
			response.setStatus((HttpStatus) o);
		} else if(o instanceof String) {
			if(request.isGet()) {
				return new StringRepresentation((String) o);
			} else if(request.isPost() || request.isPut() || request.isDelete()) {
				return new SeeOtherRepresentation((String) o);
			}
		} else if(o != null) {
			return new SerializedRepresentation(o);
		}
		return null;
	}

	protected static List<Field> getFields(Class clazz) {
		List<Field> fields = new ArrayList<Field>();
		while(clazz != null && !clazz.equals(Object.class)) {
			fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
			clazz = clazz.getSuperclass();
		}
		return fields;
	}

	private static String getAcceptString(Class clazz) {
		List<String> accept = new ArrayList<String>();
		for(Method method : clazz.getMethods()) {
			if(!method.getDeclaringClass().equals(Object.class)) {
				accept.add(method.getName().toUpperCase());
			}
		}

		accept.add("HEAD");
		accept.add("OPTIONS");

		StringBuffer buffer = new StringBuffer();
		for(Iterator<String> it = accept.iterator(); it.hasNext();) {
			buffer.append(it.next());
			if(it.hasNext()) {
				buffer.append(", ");
			}
		}
		return buffer.toString();
	}

	protected static Object convertValue(Class<?> type, String value) {
		if(String.class.equals(type)) {
			return value;
		} else if(int.class.equals(type) || Integer.class.equals(type)) {
			return new Integer(value);
		} else if(long.class.equals(type) || Long.class.equals(type)) {
			return new Long(value);
		} else if(double.class.equals(type) || Double.class.equals(type)) {
			return new Double(value);
		} else if(float.class.equals(type) || Float.class.equals(type)) {
			return new Float(value);
		} else if(boolean.class.equals(type) || Boolean.class.equals(type)) {
			return "1".equals(value) || "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
		}
		return null;
	}

	private static String extractPathFromRequestURI(HttpServletRequest request) {
		String contextPath = request.getContextPath();
		String uri = extractURI(request);
		return uri.substring(uri.indexOf(contextPath) + contextPath.length());
	}

	/**
	 * details in : http://www.caucho.com/resin-3.0/webapp/faq.xtp#forward-path
	 * and http://www.javaworld.com/javaworld/jw-03-2003/jw-0328-servlet.html?page=4
	 *
	 * @param request
	 * @return
	 */
	private static String extractURI(HttpServletRequest request) {
		String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI);

		if(uri != null) {
			return uri;
		} else {
			return request.getRequestURI();
		}
	}
}
